package zhongbo.javaweb.instagram.util;

import lombok.Getter;
import lombok.Setter;
import org.apache.http.Header;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.brotli.dec.BrotliInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import zhongbo.javaweb.instagram.util.dns.CustomDnsResolver;

import javax.net.ssl.SSLContext;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;

/**
 * @author zhongbo
 * @since 2019/1/8 0008
 */
@Getter
@Setter
public class HttpClientUtil {

    private static Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);

    private static Integer maxConn = 500;
    private static Integer maxPerRoute = 2000;
    private static Integer defaultTimeOut = 20000;

    private static CloseableHttpClient httpClient = createHttpClient(maxConn, maxPerRoute);
    private static CloseableHttpClient httpsClient = createHttpSslClient(maxConn, maxPerRoute);
    private static CloseableHttpClient httpInsClient = createInsClient();

    private static final String HTTP_PROTOCOL_HTTP = "http";
    private static final String HTTP_PROTOCOL_HTTPS = "https";

    public static final String CONTENT_ENCODING_BR = "br";
    public static final String CONTENT_ENCODING = "Content-Encoding";

    public static byte[] doGet(String url) throws IOException {
        if (url != null && url.length() > 0) {
            CloseableHttpClient client = chooseClient(url);
            HttpGet httpGet = new HttpGet(url);
            CloseableHttpResponse execute = client.execute(httpGet);
            if (execute.getStatusLine().getStatusCode() == HttpStatus.OK.value()) {
                return EntityUtils.toByteArray(execute.getEntity());
            }
        }
        return null;
    }

    public static String doGetString(String url) throws IOException {
        if (url != null && url.length() > 0) {
            CloseableHttpClient client = chooseClient(url);
            HttpGet httpGet = new HttpGet(url);
            CloseableHttpResponse execute = client.execute(httpGet);
            byte[] bodyBytes;
            if (execute.getStatusLine().getStatusCode() == HttpStatus.OK.value()) {
                byte[] bytes = EntityUtils.toByteArray(execute.getEntity());
                String contentEncoding = getContentEncoding(execute);
                if (CONTENT_ENCODING_BR.equals(contentEncoding)) {
                    bodyBytes = deCompressBrotli(bytes);
                } else {
                    bodyBytes = bytes;
                }
                if (null != bodyBytes) {
                    Header firstHeader = execute.getFirstHeader(CONTENT_ENCODING);
                    String body;
                    if (firstHeader != null && firstHeader.getValue() != null) {
                        String value = firstHeader.getValue();
                        body = new String(bodyBytes, value);
                    } else {
                        body = new String(bodyBytes);
                    }
                    return body;
                }
            } else {
                logger.error("failed status code: {} url: {}", execute.getStatusLine().getStatusCode(), url);
            }
        }
        logger.error("doGetString failed! null");
        return null;
    }

    /**
     * 获取http数据内容压缩类型
     *
     * @param httpMessage http数据
     * @return 压缩类型
     */
    public static String getContentEncoding(HttpMessage httpMessage) {
        String encodingHeaderValue = null;
        Header encodingHeader = httpMessage.getFirstHeader("Content-Encoding");
        if (null != (encodingHeader)) {
            encodingHeaderValue = encodingHeader.getValue();
        }
        return encodingHeaderValue;
    }

    /**
     * 解压缩 Brotli 压缩数据
     * <p>
     * #link https://www.cnblogs.com/wcrBlog/p/11805506.html
     *
     * @param sourceBytes 压缩数据
     * @return 解压数据
     */
    private static byte[] deCompressBrotli(byte[] sourceBytes) throws IOException {
        try {
            if (null != (sourceBytes)) {

                byte[] brotliBytes = IoUtils.readFully(new BrotliInputStream(new ByteArrayInputStream(sourceBytes)), -1, true);
                if (null != (brotliBytes)) {
                    logger.debug("Brotli 解压前大小: {} 解压后大小: {}", sourceBytes.length, brotliBytes.length);
                    return brotliBytes;
                }
            } else {
                logger.warn("Brotli 解压数据为空");
            }
        } catch (IOException e) {
            logger.error("解压 Brotli 数据失败! error: ", e);
            throw e;
        }
        return null;
    }

    public static CloseableHttpClient chooseClient(String url) {
        if (url.toLowerCase().startsWith(HTTP_PROTOCOL_HTTPS)) {
            return httpsClient;
        }
        return httpClient;
    }

    public static CloseableHttpClient chooseInsClient() {
        System.setProperty("https.protocols", "TLSv1.3");
        if (httpInsClient == null) {
            httpInsClient = createInsClient();
        }
        return httpInsClient;
    }


    /**
     * 创建带连接池的安全链接httpclient。
     *
     * @return httpClient 客户端
     */
    public static CloseableHttpClient createInsClient() {
        try {
            SSLContext sslcontext = new SSLContextBuilder().build();

            // Allow TLSv1 protocol only
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext,
                    new String[]{"TLSv1.3"}, null,
                    SSLConnectionSocketFactory.getDefaultHostnameVerifier());
            return HttpClients.custom().setSSLSocketFactory(sslsf).build();
        } catch (Exception e) {
            logger.error("创建ins httpClient 失败!", e);
        }
        return null;
    }


    /**
     * 创建带连接池的安全链接httpclient。
     *
     * @param maxConn     最大的链接数。
     * @param maxPerRoute 单个路由最大的链接数。
     * @return httpClient 客户端
     */
    public static CloseableHttpClient createHttpSslClient(Integer maxConn, Integer maxPerRoute) {
        SSLContextBuilder builder = new SSLContextBuilder();
        CloseableHttpClient httpsClient = null;
        try {
            TrustStrategy trustStrategy = (x509Certificates, authType) -> {
                logger.debug(" authType: {}", authType);
                return true;
            };
            // 初始化https客户端，全部信任 不做身份鉴定
            builder.loadTrustMaterial(null, trustStrategy);
            SSLConnectionSocketFactory sslSf = new SSLConnectionSocketFactory(builder.build(), new String[]{"SSLv3", "TLSv1", "TLSv1.2", "TLSv1.3"}, null, NoopHostnameVerifier.INSTANCE);
            Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register(HTTP_PROTOCOL_HTTP, new PlainConnectionSocketFactory())
                    .register(HTTP_PROTOCOL_HTTPS, sslSf)
                    .build();
            PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registry, CustomDnsResolver.builder().build());
            cm.setMaxTotal(maxConn);
            cm.setDefaultMaxPerRoute(maxPerRoute);
            httpsClient = HttpClients.custom()
                    .setSSLSocketFactory(sslSf)
                    .setConnectionManager(cm)
                    .setConnectionManagerShared(true)
                    .build();
        } catch (NoSuchAlgorithmException e) {
            logger.warn("NoSuchAlgorithmException: ", e);
        } catch (KeyStoreException e) {
            logger.warn("KeyStoreException: ", e);
        } catch (KeyManagementException e) {
            logger.warn("KeyManagementException: ", e);
        }
        return httpsClient;
    }


    /**
     * 创建带连接池的httpclient。
     *
     * @param maxConn     最大的链接数。
     * @param maxPerRoute 单个路由最大的链接数。
     * @return httpClient 客户端
     */
    public static CloseableHttpClient createHttpClient(Integer maxConn, Integer maxPerRoute) {
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register(HTTP_PROTOCOL_HTTP, new PlainConnectionSocketFactory())
                .build();
        // 初始化http客户端
        // @link https://blog.csdn.net/rzleilei/article/details/19546903 指定DnsResolver
        PoolingHttpClientConnectionManager cmOfHttp = new PoolingHttpClientConnectionManager(registry, CustomDnsResolver.builder().build());
        cmOfHttp.setMaxTotal(maxConn);
        cmOfHttp.setDefaultMaxPerRoute(maxPerRoute);
        //不自动重定向, 也可以在RequestConfig中配置
        RedirectStrategy noRedirectStrategy = buildNoRedirect();
        logger.debug("noRedirectStrategy disabled: {}", noRedirectStrategy);
        long timeoutMs = TimeUnit.SECONDS.toMillis(5);
        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(cmOfHttp)
                .setConnectionManagerShared(true)
                .setDefaultSocketConfig(buildSocketTimeOutConfig(Math.toIntExact(timeoutMs)))
                .setDefaultRequestConfig(buildTimeOutConfig(Math.toIntExact(timeoutMs)))
                .setConnectionTimeToLive(30, TimeUnit.SECONDS)
                //.setRedirectStrategy(noRedirectStrategy)
                .build();
        logger.debug("created httpClient: {}", httpClient);
        return httpClient;
    }

    private static RedirectStrategy buildNoRedirect() {
        RedirectStrategy noRedirectStrategy = new RedirectStrategy() {
            @Override
            public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) {
                return true;
            }

            @Override
            public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) {
                HttpRequestBase request1 = (HttpRequestBase) request;
                final URI uri = request1.getURI();
                final String method = request.getRequestLine().getMethod();
                if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) {
                    return new HttpHead(uri);
                } else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) {
                    return new HttpGet(uri);
                } else {
                    final int status = response.getStatusLine().getStatusCode();
                    if (status == org.apache.http.HttpStatus.SC_TEMPORARY_REDIRECT) {
                        return RequestBuilder.copy(request).setUri(uri).build();
                    } else {
                        return new HttpGet(uri);
                    }
                }
            }
        };
        logger.debug("noRedirectStrategy: {}", noRedirectStrategy);
        return noRedirectStrategy;
    }

    /**
     * 获取超时配置
     *
     * @param timeoutMs 超时时间(毫秒)
     * @return 请求配置信息
     */
    private static RequestConfig buildTimeOutConfig(int timeoutMs) {
        RequestConfig timeOutConfig;
        if (timeoutMs != 0) {
            timeOutConfig = RequestConfig
                    .custom()
                    .setSocketTimeout(timeoutMs)
                    .setConnectTimeout(timeoutMs)
                    .setConnectionRequestTimeout(timeoutMs)
                    .build();
        } else {
            //默认超时时间20秒
            timeOutConfig = RequestConfig.DEFAULT;
        }
        return timeOutConfig;
    }

    /**
     * 获socket取超时配置
     * 配置此方法修复 httpClient.execute() 方法卡死进程,导致进程无法终止问题
     * <p>
     * #link https://blog.csdn.net/s906199800/article/details/83379437
     *
     * @param timeoutMs 超时时间(毫秒)
     * @return 请求配置信息
     */
    private static SocketConfig buildSocketTimeOutConfig(int timeoutMs) {
        SocketConfig socketConfig;
        if (timeoutMs != 0) {
            socketConfig = SocketConfig.custom()
                    .setSoKeepAlive(false)
                    .setSoLinger(1)
                    .setSoReuseAddress(true)
                    .setSoTimeout(timeoutMs)
                    .setTcpNoDelay(true).build();
        } else {
            //默认超时时间20秒
            socketConfig = SocketConfig.DEFAULT;
        }
        return socketConfig;
    }
}
